<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>Three.js - Align HTML Elements to 3D Globe</title>
    <style>
    html, body {
        height: 100%;
        margin: 0;
        font-family: sans-serif;
    }
    #c {
        width: 100%;  /* let our container decide our size */
        height: 100%;
        display: block;
    }
    #container {
      position: relative;  /* makes this the origin of its children */
      width: 100%;
      height: 100%;
      overflow: hidden;
    }
    #labels {
      position: absolute;  /* let us position ourself inside the container */
      z-index: 0;          /* make a new stacking context so children don't sort with rest of page */
      left: 0;             /* make our position the top left of the container */
      top: 0;
      color: white;
    }
    #labels>div {
      position: absolute;  /* let us position them inside the container */
      left: 0;             /* make their default position the top left of the container */
      top: 0;
      cursor: pointer;     /* change the cursor to a hand when over us */
      font-size: small;
      user-select: none;   /* don't let the text get selected */
      pointer-events: none;  /* make us invisible to the pointer */
      text-shadow:         /* create a black outline */
        -1px -1px 0 #000,
         0   -1px 0 #000,
         1px -1px 0 #000,
         1px  0   0 #000,
         1px  1px 0 #000,
         0    1px 0 #000,
        -1px  1px 0 #000,
        -1px  0   0 #000;
    }
    #labels>div:hover {
      color: red;
    }
    </style>
  </head>
  <body>
    <div id="container">
      <canvas id="c"></canvas>
      <div id="labels"></div>
    </div>
  </body>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>

<script type="importmap">
{
  "imports": {
    "three": "../../build/three.module.js",
    "three/addons/": "../../examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({canvas});

  const fov = 60;
  const aspect = 2;  // the canvas default
  const near = 0.1;
  const far = 10;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.z = 2.5;

  const controls = new OrbitControls(camera, canvas);
  controls.enableDamping = true;
  controls.enablePan = false;
  controls.minDistance = 1.2;
  controls.maxDistance = 4;
  controls.update();

  const scene = new THREE.Scene();
  scene.background = new THREE.Color('#236');

  {
    const loader = new THREE.TextureLoader();
    const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
    const geometry = new THREE.SphereGeometry(1, 64, 32);
    const material = new THREE.MeshBasicMaterial({map: texture});
    scene.add(new THREE.Mesh(geometry, material));
  }

  async function loadJSON(url) {
    const req = await fetch(url);
    return req.json();
  }

  let countryInfos;
  async function loadCountryData() {
    countryInfos = await loadJSON('resources/data/world/country-info.json');  /* threejs.org: url */

    const lonFudge = Math.PI * 1.5;
    const latFudge = Math.PI;
    // these helpers will make it easy to position the boxes
    // We can rotate the lon helper on its Y axis to the longitude
    const lonHelper = new THREE.Object3D();
    // We rotate the latHelper on its X axis to the latitude
    const latHelper = new THREE.Object3D();
    lonHelper.add(latHelper);
    // The position helper moves the object to the edge of the sphere
    const positionHelper = new THREE.Object3D();
    positionHelper.position.z = 1;
    latHelper.add(positionHelper);

    const labelParentElem = document.querySelector('#labels');
    for (const countryInfo of countryInfos) {
      const {lat, lon, name} = countryInfo;

      // adjust the helpers to point to the latitude and longitude
      lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
      latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;

      // get the position of the lat/lon
      positionHelper.updateWorldMatrix(true, false);
      const position = new THREE.Vector3();
      positionHelper.getWorldPosition(position);
      countryInfo.position = position;

      // add an element for each country
      const elem = document.createElement('div');
      elem.textContent = name;
      labelParentElem.appendChild(elem);
      countryInfo.elem = elem;
    }
    requestRenderIfNotRequested();
  }
  loadCountryData();

  const tempV = new THREE.Vector3();

  function updateLabels() {
    // exit if we have not yet loaded the JSON file
    if (!countryInfos) {
      return;
    }

    for (const countryInfo of countryInfos) {
      const {position, elem} = countryInfo;

      // get the normalized screen coordinate of that position
      // x and y will be in the -1 to +1 range with x = -1 being
      // on the left and y = -1 being on the bottom
      tempV.copy(position);
      tempV.project(camera);

      // convert the normalized position to CSS coordinates
      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;

      // move the elem to that position
      elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;

      // set the zIndex for sorting
      elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
    }
  }

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }
    return needResize;
  }

  let renderRequested = false;

  function render() {
    renderRequested = undefined;

    if (resizeRendererToDisplaySize(renderer)) {
      const canvas = renderer.domElement;
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      camera.updateProjectionMatrix();
    }

    controls.update();

    updateLabels();

    renderer.render(scene, camera);
  }
  render();

  function requestRenderIfNotRequested() {
    if (!renderRequested) {
      renderRequested = true;
      requestAnimationFrame(render);
    }
  }

  controls.addEventListener('change', requestRenderIfNotRequested);
  window.addEventListener('resize', requestRenderIfNotRequested);
}

main();
</script>
</html>

